package team.tttxs.ct.hibernate.dao.impl;

/** 
 * 
 * @author nico
 * @version createTime：2018年3月31日 上午12:08:32
 */

import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.annotation.Version;

import team.tttxs.ct.hibernate.dao.BaseDao;
import team.tttxs.ct.hibernate.dao.util.DBHelper;
import team.tttxs.ct.hibernate.dao.util.Page;

public class BaseDaoImpl<T> implements BaseDao<T> {
	
	private Class<T> entityClass;
	
	@PersistenceContext
	private EntityManager em;
	
	public EntityManager getEm() {
		return em;
	}

	public void setEm(EntityManager em) {
		this.em = em;
	}

	public BaseDaoImpl() {
		//TODO, need more test
		entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
	}

	@Override
	public T get(String id) {
		//T t = em.find(entityClass, id);
		//return t;
		return em.find(entityClass, id);
	}

	@Override
	public List<T> get(List<String> ids) {
		//TypedQuery<T> q = em.createQuery("find t from T t where t.id in ids", entityClass);
		//return q.getResultList();
		Map<String, Object> queryOptions = new HashMap<String, Object>();
		queryOptions.put("id", ids);
		return get(queryOptions, null);
	}

	@Override
	public List<T> get(Map<String, Object> queryOptions, Page page) {
		//TODO: use page
		
		CriteriaBuilder cb = em.getCriteriaBuilder();
		CriteriaQuery<T> cq = cb.createQuery(entityClass);
		Root<T> t = cq.from(entityClass);
		//EntityType<T> t_ = t.getModel();
		Predicate condition = null;
		if(queryOptions != null && !queryOptions.isEmpty() && queryOptions.size() > 0) {
			for(String key : queryOptions.keySet()) {
				Object value = queryOptions.get(key);
				Predicate cri = null;
				
				//some special search operator
				if(value instanceof String && ((String)value).startsWith("?")) {
					String strValue = (String)value;
					String searchKey = strValue.substring(1, strValue.length());
					Expression<String> ex = t.get(key);
					cri = cb.like(ex, "%" + searchKey + "%");
				} else if(value instanceof List) {
					List listValue = (List)value;
					Expression<String> exp = t.get(key);
					cri = exp.in(listValue);
				} else if(value instanceof String && ((String)value).startsWith("&")){
					String strValue = (String)value;
					String searchKey = strValue.substring(1, strValue.length());
					Expression<String> ex = t.get(key);
					cri = cb.notLike(ex, searchKey);
				}else {
					cri = cb.equal(t.get(key), value);
				}

				if(condition == null) {
					condition = cri;
				} else {
					condition = cb.and(condition, cri);
				}
			}
			
			cq.where(condition);
		}
		
		List<Order> orderList = new ArrayList<Order>();
		if(page != null){
			if(page.isSortable()) {
				for(String str : page.getOrderClauses().keySet()) {
					//query.sort().on(str, page.getOrderClauses().get(str));
					Integer direction = page.getOrderClauses().get(str);
					if(direction <= 0){
						orderList.add(cb.desc(t.get(str)));
					}
					else{
						orderList.add(cb.asc(t.get(str)));
					}
				}
				if(orderList.size() > 0){
					cq.orderBy(orderList);
				}
			}
		}
		TypedQuery<T> q = em.createQuery(cq);
		//paging support
		if(page != null) {
			q.setFirstResult(page.getOffset());
			if(page.getPagesize() > 0){
				q.setMaxResults(page.getPagesize());
			}
		}
		
		return q.getResultList();
	}

	@Override
	public void save(T obj) {
		em.merge(obj);
		em.flush();
	}

	@Override
	public T insert(T obj) {
		em.persist(obj);
		em.flush();
		return obj;
	}

	@Override
	public void remove(String id) {
		T target = get(id);
		if(target != null)
			em.remove(target);
		em.flush();
	}

	@Override
	public int update(String id, Integer revision, Map<String, Object> updateMap) {
		//TODO, locking ? Optimistic, pessimistic
		T target = em.find(entityClass, id);
		
		if(target == null) {
			return 0;
		}
		
		String fieldName = DBHelper.getAnnotatedField(entityClass, Version.class);
		
		//TODO: optimistic lock support, no lock support if revision is not present, or no Version defined in Domain class
		//below will not work in JPA, need a new way
		try {
			if(revision != null){
				int _v = entityClass.getField(fieldName).getInt(target);
				if(_v != revision) {
					throw new OptimisticLockingFailureException("optimistic locking failure - stale object");
				}
			}
		} catch (Exception ex) {
			//no revision attribute defined, do nothing
		}
	
		//updateMap shouldn't have id
//		jsonConverter.updateObject(jsonConverter.toJson(updateMap), target);
		em.merge(target);
		em.flush();
		return 1;
	}

	@Override
	public long count(Map<String, Object> queryOptions) {
		CriteriaBuilder cb = em.getCriteriaBuilder();  
		CriteriaQuery<Long> cq = cb.createQuery(Long.class);  
		Root<T> t = cq.from(entityClass);  
		cq.select(cb.countDistinct(t));  
		Predicate condition = null;
		if(queryOptions != null && !queryOptions.isEmpty() && queryOptions.size() > 0) {
			for(String key : queryOptions.keySet()) {
				Object value = queryOptions.get(key);
				Predicate cri = null;
				
				//some special search operator
				if(value instanceof String && ((String)value).startsWith("?")) {
					String strValue = (String)value;
					String searchKey = strValue.substring(1, strValue.length());
					Expression<String> ex = t.get(key);
					cri = cb.like(ex, "%" + searchKey + "%");
				} else if(value instanceof List) {
					List listValue = (List)value;
					if(listValue.get(0) instanceof Date && listValue.size() == 2){
						Expression<Date> k = t.get(key);
						cri = cb.between(k, (Date)listValue.get(0), (Date)listValue.get(1));
					}else{
						Expression<String> exp = t.get(key);
						cri = exp.in(listValue);
					}
				} else {
					cri = cb.equal(t.get(key), value);
				}

				if(condition == null) {
					condition = cri;
				} else {
					condition = cb.and(condition, cri);
				}
			}
			cq.where(condition);
		}
		
		long count = em.createQuery(cq).getSingleResult().intValue();
		return count;
	}
	
	
	/**
	 * The element in List could be an Object[] or an Object, depends on the SQL
	 * 
	 * @param sql, the native SQL to run in database
	 * @return
	 */
	@SuppressWarnings("unchecked")
	@Override
	public List<T> nativeQuery(String sql, List<Object> args) {
		Query q = em.createNativeQuery(sql, entityClass);
		if(args != null && args.size() > 0){
			for(int i = 1; i <= args.size(); i++){
				q.setParameter(i, args.get(i- 1));
			}
		}
		
		return q.getResultList();
	}

	@Override
	public int nativeNonQuery(String sql, List<Object> args) {
		// TODO Auto-generated method stub
		Query q = em.createNativeQuery(sql);
		if(args != null && args.size() > 0){
			for(int i = 1; i <= args.size(); i++){
				q.setParameter(i, args.get(i- 1));
			}
		}
		return q.executeUpdate();
	}

	@Override
	public void insert(T obj, String collectionName) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public List<T> get(Map<String, Object> queryOptions, Page page,
			String collectionName) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public int update(String id, Integer revision,
			Map<String, Object> updateMap, String collectionName) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public void remove(String id, String collectionName) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void save(T obj, String collectionName) {
		// TODO Auto-generated method stub
		
	}

}

